package com.googlecode.afx.view.validation.impl;

import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;

import javafx.scene.Node;
import javafx.scene.control.Control;

import org.apache.commons.lang3.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.controlsfx.validation.ValidationMessage;
import org.controlsfx.validation.ValidationResult;
import org.controlsfx.validation.ValidationSupport;
import org.controlsfx.validation.Validator;
import org.controlsfx.validation.decoration.CompoundValidationDecoration;
import org.controlsfx.validation.decoration.GraphicValidationDecoration;
import org.controlsfx.validation.decoration.StyleClassValidationDecoration;
import org.controlsfx.validation.decoration.ValidationDecoration;
import org.springframework.util.ReflectionUtils;
import org.springframework.util.ReflectionUtils.FieldCallback;

import com.googlecode.afx.AFXMessageSource;
import com.googlecode.afx.annotation.AFXRequiredValidation;
import com.googlecode.afx.annotation.AFXRequiredValidations;
import com.googlecode.afx.utils.AnnotationUtils;
import com.googlecode.afx.utils.AFXUtils;
import com.googlecode.afx.view.DialogUtils;
import com.googlecode.afx.view.validation.ValidationException;
import com.googlecode.afx.view.validation.ValidationHandler;
import com.googlecode.afx.view.validation.ValidationItem;

/**
 * Implementation of the <tt>ValidationHandler</tt> interface, making use of the ControlsFX validation framework.
 * 
 * @author MartinKoster
 *
 */
public class ValidationHandlerImpl implements ValidationHandler {

	private static final Log LOG = LogFactory.getLog(ValidationHandlerImpl.class);

	private ValidationSupport validationSupport;

	private Object modelHolder;

	private Node root;

	private AFXMessageSource messageSource;

	public ValidationHandlerImpl(final Object modelHolder, final Node root, final AFXMessageSource messageSource) {
		this.modelHolder = modelHolder;
		this.root = root;
		this.messageSource = messageSource;
		ValidationDecoration iconDecorator = new GraphicValidationDecoration();
		ValidationDecoration cssDecorator = new StyleClassValidationDecoration();
		ValidationDecoration compoundDecorator = new CompoundValidationDecoration(cssDecorator, iconDecorator);
		this.validationSupport = new ValidationSupport();
		this.validationSupport.setValidationDecorator(compoundDecorator);
		this.registerValidators();
	}

	/**
	 * Registers all validators defined in the controller via annotations in the
	 * view nodes.
	 * 
	 * @param bean
	 * @param view
	 * @param messageSource
	 */
	protected void registerValidators() {
		ValidatorRegistrar registrar = new ValidatorRegistrar(root, messageSource);
		ReflectionUtils.doWithFields(this.modelHolder.getClass(), registrar);
	}

	/**
	 * Returns <tt>true</tt>, if there are validation errors, <tt>false</tt>
	 * otherwise.
	 * 
	 * @return
	 */
	@Override
	public boolean hasValidationErrors() {
		ValidationResult result = this.validationSupport.validationResultProperty().get();
		return result == null ? false : result.getErrors() != null && result.getErrors().size() > 0;
	}

	/**
	 * Returns <tt>true</tt>, if there are validation errors for the node
	 * identified by <tt>nodeId</tt>, <tt>false</tt> otherwise.
	 * 
	 * @return
	 */
	@Override
	public boolean hasValidationErrors(String nodeId) {
		if (this.hasValidationErrors()) {
			ValidationResult result = this.validationSupport.validationResultProperty().get();
			for (ValidationMessage validationMessage : result.getMessages()) {
				
				if (validationMessage.getTarget() != null && validationMessage.getTarget().getId() != null
						&& validationMessage.getTarget().getId().equals(nodeId)) {
					return true;
				}
			}
		}
		return false;
	}

	/**
	 * Constructs and throws an instance of <tt>ValidationException</tt> in case
	 * there are validation errors.
	 */
	@Override
	public void throwValidationExceptionIfHasErrors() throws ValidationException {
		if (this.hasValidationErrors()) {
			throw new ValidationException(this.getValidationItems());
		}
	}

	/**
	 * Returns the list of validation messages.
	 * 
	 * @return
	 */
	@Override
	public List<String> getValidationErrorMessages() {
		ValidationResult result = this.validationSupport.validationResultProperty().get();
		List<String> messages = new ArrayList<String>();
		if (this.hasValidationErrors()) {
			for (ValidationMessage validationMessage : result.getMessages()) {
				messages.add(validationMessage.getText());
			}
		}
		return messages;
	}

	/**
	 * Returns the list of <tt>ValidationItem</tt>s.
	 * 
	 * @return
	 */
	@Override
	public List<ValidationItem> getValidationItems() {
		ValidationResult result = this.validationSupport.validationResultProperty().get();
		List<ValidationItem> items = new ArrayList<ValidationItem>();
		for (ValidationMessage validationMessage : result.getMessages()) {
			items.add(new ValidationItem(validationMessage.getText(), validationMessage.getTarget()));
		}
		return items;
	}

	/**
	 * Returns all validation messages combined into one string for display.
	 * 
	 * @return
	 */
	@Override
	public String getValidationErrorMessagesForDisplay() {
		StringBuilder b = new StringBuilder();
		for (String message : this.getValidationErrorMessages()) {
			b.append(message).append("\n");
		}
		return b.toString();
	}

	/**
	 * Returns all validation messages combined into one string for display.
	 * 
	 * @return
	 */
	@Override
	public String getValidationErrorMessagesForDisplay(ValidationException exception) {
		StringBuilder b = new StringBuilder();
		for (ValidationItem item : exception.getValidationItems()) {
			b.append(item.getMessage()).append("\n");
		}
		return b.toString();
	}

	/**
	 * Displays a modal error dialog containing the validation errors associated
	 * with the instance of <tt>ValidationSupport</tt>.
	 */
	@Override
	public void showValidationErrorDialog() {
		String title = this.messageSource.getMessage("message_validation_dialog", null);
		DialogUtils.showErrorDialog(title, this.getValidationErrorMessagesForDisplay());
	}

	/**
	 * Displays a modal error dialog containing the validation errors.
	 * 
	 * @param exception
	 *            the <tt>ValidationException</tt> to be displayed in the error
	 *            dialog.
	 */
	@Override
	public void showValidationErrorDialog(ValidationException exception) {
		String title = this.messageSource.getMessage("message_validation_dialog", null);
		DialogUtils.showErrorDialog(title, this.getValidationErrorMessagesForDisplay(exception));
	}

	/**
	 * Field-level validator that is validating the field against the applied
	 * validations.
	 * 
	 * @author MartinKoster
	 *
	 */
	private class ValidatorRegistrar implements FieldCallback {

		private Node root;

		private AFXMessageSource messageSource;

		public ValidatorRegistrar(Node root, AFXMessageSource messageSource) {
			this.root = root;
			this.messageSource = messageSource;
		}

		@Override
		public void doWith(Field field) throws IllegalArgumentException, IllegalAccessException {
			this.registerNotEmptyValidators(field);
		}

		/**
		 * Validate, whether mandatory fields are not empty.
		 * 
		 * @param field
		 */
		protected void registerNotEmptyValidators(Field field) {
			Set<AFXRequiredValidation> annotations = AnnotationUtils.getRepeatableAnnotation(field,
					AFXRequiredValidations.class, AFXRequiredValidation.class);
			if (annotations == null || annotations.size() == 0) {
				return;
			}

			for (AFXRequiredValidation annotation : annotations) {
				String nodeId = this.getLookupNodeId(field, annotation.path());

				// register validator
				Node node = AFXUtils.findNodeById(root, nodeId);
				if (node instanceof Control) {
					LOG.debug("Applying NotEmptyValidator to node with id='" + nodeId + "', message code is '"
							+ annotation.code() + "'");
					validationSupport.registerValidator((Control) node,
							Validator.createEmptyValidator(this.getMessage(annotation.code())));
				}
			}
		}

		/**
		 * Determines the node ID that needs to be checked for the given
		 * <tt>field</tt> and the supplied <tt>path</tt>.
		 * 
		 * @param field
		 * @param path
		 * @return
		 */
		protected String getLookupNodeId(Field field, String path) {
			// construct nested path to lookup values relative to the annotated
			// field
			String nodeId = (StringUtils.trimToNull(path) == null) ? field.getName() : field.getName() + "." + path;
			return nodeId;
		}

		public String getMessage(String code) {
			return this.getMessageSource().getMessage(code, null);
		}

		public AFXMessageSource getMessageSource() {
			return messageSource;
		}
	}

}
